import * as ansi from 'tui-lib/util/ansi'
import telc from 'tui-lib/util/telchars'
import unic from 'tui-lib/util/unichars'

import Form from './Form.js'
import ScrollBar from './ScrollBar.js'

export default class ListScrollForm extends Form {
  // A form that lets the user scroll through a list of items. It
  // automatically adjusts to always allow the selected item to be visible.
  // Unless disabled in the constructor, a scrollbar is automatically displayed
  // if there are more items than can be shown in the height of the form at a
  // single time.

  constructor(layoutType = 'vertical', enableScrollBar = true) {
    super()

    this.layoutType = layoutType
    this.wheelMode = 'scroll' // scroll, selection

    this.scrollItems = 0

    this.scrollBarEnabled = enableScrollBar

    this.scrollBar = new ScrollBar({
      getLayoutType: () => this.layoutType,
      getCurrentScroll: () => this.scrollItems,
      getMaximumScroll: () => this.getScrollItemsLength(),
      getTotalItems: () => this.inputs.length
    })
    this.scrollBarShown = false
  }

  fixLayout() {
    this.keepScrollInBounds()

    const scrollItems = this.scrollItems

    // The scrollItems property represents the item to the very left of where
    // we've scrolled, so we know right away that none of those will be
    // visible and we won't bother iterating over them. We do need to hide
    // them, though.
    for (let i = 0; i < Math.min(scrollItems, this.inputs.length); i++) {
      this.inputs[i].visible = false
    }

    // This variable stores how far along the respective axis (implied by
    // layoutType) the next element should be.
    let nextPos = 0

    let formEdge
    if (this.layoutType === 'horizontal') {
      formEdge = this.contentW
    } else {
      formEdge = this.contentH
    }

    for (let i = scrollItems; i < this.inputs.length; i++) {
      const item = this.inputs[i]
      item.fixLayout()

      const curPos = nextPos
      let curSize
      if (this.layoutType === 'horizontal') {
        item.x = curPos
        curSize = item.w
      } else {
        item.y = curPos
        curSize = item.h
      }
      nextPos += curSize

      // By default, the item should be visible..
      item.visible = true

      // ..but the item's far edge is past the form's far edge, it isn't
      // fully visible and should be hidden.
      if (curPos + curSize > formEdge) {
        item.visible = false
      }

      // Same deal goes for the close edge. We can check it against 0 since
      // the close edge of the form's content is going to be 0, of course!
      if (curPos < 0) {
        item.visible = false
      }
    }

    delete this._scrollItemsLength

    if (this.scrollBarEnabled) {
      this.showScrollbarIfNecessary()
    }
  }

  keyPressed(keyBuf) {
    let ret

    handleKeyPress: {
      if (this.layoutType === 'horizontal') {
        if (telc.isLeft(keyBuf)) {
          this.previousInput()
          ret = false; break handleKeyPress
        } else if (telc.isRight(keyBuf)) {
          this.nextInput()
          ret = false; break handleKeyPress
        }
      } else if (this.layoutType === 'vertical') {
        if (telc.isUp(keyBuf)) {
          this.previousInput()
          ret = false; break handleKeyPress
        } else if (telc.isDown(keyBuf)) {
          this.nextInput()
          ret = false; break handleKeyPress
        }
      }

      ret = super.keyPressed(keyBuf)
    }

    this.scrollSelectedElementIntoView()

    return ret
  }

  clicked(button) {
    if (this.wheelMode === 'selection') {
      // Change the actual selected item.
      if (button === 'scroll-up') {
        this.previousInput()
        this.scrollSelectedElementIntoView()
      } else if (button === 'scroll-down') {
        this.nextInput()
        this.scrollSelectedElementIntoView()
      }
    } else if (this.wheelMode === 'scroll') {
      // Scrolling is typically pretty slow with a mouse wheel when it's by
      // a single line, so scroll at 3x that speed.
      for (let i = 0; i < 3; i++) {
        if (button === 'scroll-up') {
          this.scrollItems--
        } else if (button === 'scroll-down') {
          this.scrollItems++
        } else {
          return
        }
      }
    }

    this.fixLayout()
  }

  scrollSelectedElementIntoView() {
    const sel = this.inputs[this.curIndex]

    if (!sel) {
      return
    }

    let formEdge
    if (this.layoutType === 'horizontal') {
      formEdge = this.contentW
    } else {
      formEdge = this.contentH
    }

    // If the item is ahead of our view (either to the right of or below),
    // we should move the view so that the item is the farthest right (of all
    // the visible items).
    if (this.getItemPos(sel) > formEdge + this.scrollSize) {
      this.scrollElementIntoEndOfView(sel)
    }

    // Adjusting the number of scroll items is much simpler to deal with if
    // the item is behind our view. Since the item's behind, we need to move
    // the scroll to be immediately behind it, which is simple since we
    // already have its index.
    if (this.getItemPos(sel) <= this.scrollSize) {
      this.scrollItems = this.curIndex
    }

    this.fixLayout()
  }

  firstInput(...args) {
    this.scrollItems = 0

    super.firstInput(...args)

    this.fixLayout()
  }

  getScrollPositionOfElementAtEndOfView(element) {
    // We can decide how many items to scroll past by moving forward until
    // the item's far edge is visible.
    const pos = this.getItemPos(element)

    let edge
    if (this.layoutType === 'horizontal') {
      edge = this.contentW
    } else {
      edge = this.contentH
    }

    for (let i = 0; i < this.inputs.length; i++) {
      if (pos <= edge) {
        return i
      }

      if (this.layoutType === 'horizontal') {
        edge += this.inputs[i].w
      } else {
        edge += this.inputs[i].h
      }
    }
    // No result? Well, it's at the end.
    return this.inputs.length
  }

  scrollElementIntoEndOfView(element) {
    this.scrollItems = this.getScrollPositionOfElementAtEndOfView(element)
  }

  scrollToBeginning() {
    this.scrollItems = 0
    this.fixLayout()
  }

  scrollToEnd() {
    this.scrollElementIntoEndOfView(this.inputs[this.inputs.length - 1])
    this.fixLayout()
  }

  keepScrollInBounds() {
    this.scrollItems = Math.max(this.scrollItems, 0)
    this.scrollItems = Math.min(this.scrollItems, this.getScrollItemsLength())
  }

  getScrollItemsLength() {
    if (typeof this._scrollItemsLength === 'undefined') {
      const lastInput = this.inputs[this.inputs.length - 1]
      this._scrollItemsLength = this.getScrollPositionOfElementAtEndOfView(lastInput)
    }

    return this._scrollItemsLength
  }

  getItemPos(item) {
    // Gets the position of the item in an unscrolled view.

    const index = this.inputs.indexOf(item)
    let pos = 0
    for (let i = 0; i <= index; i++) {
      if (this.layoutType === 'horizontal') {
        pos += this.inputs[i].w
      } else {
        pos += this.inputs[i].h
      }
    }
    return pos
  }

  showScrollbarIfNecessary() {
    this.scrollBarShown = this.scrollBar.canScrollAtAll()

    const isChild = this.children.includes(this.scrollBar)
    if (this.scrollBarShown) {
      if (!isChild) this.addChild(this.scrollBar)
    } else {
      if (isChild) this.removeChild(this.scrollBar)
    }
  }

  get scrollSize() {
    // Gets the actual length made up by all of the items currently scrolled
    // past.

    let size = 0
    for (let i = 0; i < Math.min(this.scrollItems, this.inputs.length); i++) {
      if (this.layoutType === 'horizontal') {
        size += this.inputs[i].w
      } else {
        size += this.inputs[i].h
      }
    }
    return size
  }

  get contentW() {
    if (this.scrollBarShown && this.layoutType === 'vertical') {
      return this.w - 1
    } else {
      return this.w
    }
  }

  get contentH() {
    if (this.scrollBarShown && this.layoutType === 'horizontal') {
      return this.h - 1
    } else {
      return this.h
    }
  }
}
